package data.xml;

import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.*;

import core.SemanticMatch;
import data.cad.CAD;
import data.component.*;


import java.io.File;
import java.util.stream.Collectors;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.dom4j.tree.DefaultElement;
import utils.Geometry;
import utils.Matrix;


public class XML {
    // XML的文件路径
    public String path;

    // XML Document对象
    public Document doc;

    // XML中给出的扫到的墙
    public List<Wall> walls = new ArrayList<Wall>();

    // XML给出的子墙
    public List<Wall> subWalls = new ArrayList<>();

    // XML中给出的扫到的门窗
    public List<Opening> openings = new ArrayList<Opening>();

    // 由门窗分割过后得到的墙
    public List<Wall> separatedWalls = new ArrayList<Wall>();

    // 点云旋转到XML当前位置Z轴所旋转的角度
    public Double zRotate;

    // XML中给出的二楼的墙
    public List<Wall> wallsLevel2 = new ArrayList<Wall>();

    public BoundingBox boundingBox;

    public List<Ceiling> ceilings = new ArrayList<>();
    ;

    public XML(String path) {
        this.path = path;
        File f = new File(path);
        Document doc;
        try {
            File inputFile = new File(path);
            SAXReader reader = new SAXReader();
            doc = reader.read(inputFile);

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        this.doc = doc;
        var root = doc.getRootElement();
        var wallLines = doc.selectNodes("/root/WallInfo/LineData");
        InitWalls(wallLines);
        var subLines = doc.selectNodes("/root/WallInfo/LineData/SubLine");
        if (subLines.size() != 0) {
            InitSubLines(subLines);
        }
        var doorLines = doc.selectNodes("/root/DoorInfo/DoorData");
        InitOpenings(doorLines);
        var winLines = doc.selectNodes("/root/WinInfo/WinData");
        InitOpenings(winLines);
        var ceilings = doc.selectNodes("/root/CeilingAreaInfo/CeilingAreaData");
        InitCeilings(ceilings);

        var rotate = doc.selectNodes("/root/Rotate").get(0);
        this.zRotate = Double.parseDouble(GetAttrPairs(rotate).get("Rx")) / 180 * Math.PI;

        AddOpening2Wall();

        SeparateWallByOpening();

        InitBoundingBox();

    }

    /**
     * parse all the XmlAttribute of the XmlNode to key-value map
     *
     * @param node xmlNode
     * @return attribute key-value map
     */
    private HashMap<String, String> GetAttrPairs(Node node) {
        var map = new HashMap<String, String>();
        var attrs = ((DefaultElement) node).attributes();
        for (var attr : attrs) {
            map.put(attr.getName(), attr.getValue());
        }
        return map;
    }

    /**
     * parse wallInfos XmlNode to get the wall
     *
     * @param wallLines wall`s LineData Nodes
     */
    private void InitWalls(List<Node> wallLines) {
        for (var line : wallLines) {
            var attrPairs = this.GetAttrPairs(line);
            if (!Objects.equals(attrPairs.get("grider"), "0"))
                continue;


            var wall = new Wall(attrPairs.get("WallId"), this.GetLineByAttrPairs(attrPairs), null);

            if (Objects.equals(attrPairs.get("storey"), "1")) {
                this.walls.add(wall);
                continue;
            }

            this.wallsLevel2.add(wall);
        }
    }

    private void InitSubLines(List<Node> subLines) {
        for (var subLine : subLines) {
            var attrPairs = GetAttrPairs(subLine);

            var wall = new Wall(attrPairs.get("SubLineId"), this.GetLineByAttrPairs(attrPairs), attrPairs.get("WallType"));

            this.subWalls.add(wall);
        }
    }

    /**
     * parse openingInfos XmlNode to get the wall
     *
     * @param openingLines opening`s LineData Nodes
     */
    private void InitOpenings(List<Node> openingLines) {
        for (var line : openingLines) {
            var attrPairs = this.GetAttrPairs(line);
            var opening = new Opening(attrPairs.get("HoleId"), this.GetLineByAttrPairs(attrPairs),
                    attrPairs.get("WallId"));
            this.openings.add(opening);
        }
    }

    /**
     * 解析ceiling的参数并初始化ceilings字段
     *
     * @param ceilingDatas ceiling's data node
     */
    private void InitCeilings(List<Node> ceilingDatas) {

        for (var ceilingData : ceilingDatas) {
            var ceilingAttr = this.GetAttrPairs(ceilingData);
            var corners = ((Element) ceilingData).elements("CornerData");
//            var corners = ceilingData.selectNodes("/CornerData");
            var points = new ArrayList<Double[]>();
            for (Node corner : corners) {
                var cornerPair = this.GetAttrPairs(corner);
                var point = new Double[]{Double.parseDouble(cornerPair.get("x")), Double.parseDouble(cornerPair.get("y"))};
                points.add(point);
            }
            var rate = ceilingAttr.get("Rate");
            var wallId = ceilingAttr.get("WallId");
            var ceiling = new Ceiling(points.get(0), points.get(1), points.get(2), points.get(3), Double.parseDouble(rate), wallId);
            this.ceilings.add(ceiling);
        }
    }

    /**
     * parse the XmlAttribute key-value map and get the coordinate information, return the line
     *
     * @param attrPairs the LineData`s XmlAttribute key-value map
     * @return line described by the LineData
     */
    private Line GetLineByAttrPairs(HashMap<String, String> attrPairs) {
        var start = new Double[]{Double.valueOf(attrPairs.get("StartX")), Double.valueOf(attrPairs.get("StartY"))};
        var end = new Double[]{Double.valueOf(attrPairs.get("EndX")), Double.valueOf(attrPairs.get("EndY"))};
        return new Line(start, end, false);
    }


    /**
     * Match openings and walls by the wallId of opening
     */
    private void AddOpening2Wall() {
        for (var opening : this.openings) {
            var wallList = walls.stream().filter(w -> Objects.equals(opening.wallId, w.id)).collect(Collectors.toList());
            if (wallList.size() == 0) continue;
            var wall = wallList.get(0);
            wall.openings.add(opening);
        }
    }

    /**
     * Separate wall by wall's openings to get minor wall which is with no opening.
     */
    private void SeparateWallByOpening() {
        var spWalls = new ArrayList<Wall>();

        for (var wall : walls) {
            // 特殊情况 不带门窗的墙 id乘以放大倍率后结束处理
            // 分割后的墙与原墙面的id比例
            int mainRatio = 256;
            if (wall.openings.size() == 0) {
                var spWall = new Wall((Integer.parseInt(wall.id) * mainRatio) + "", wall.start,
                        wall.end, wall.type);
                spWalls.add(spWall);
                continue;
            }

            // 将门窗的点与墙的点混合并按坐标和来排序
            var mixPoints = MixPoint.SortMixPointsByWall(wall);

            var wallNum = 1;
            for (var p : mixPoints) {
                if (p.next == null) break;

                // 几何分析得到
                if ((p.pointType == "WallStart" && p.next.pointType == "OpeningStart") ||
                        (p.pointType == "OpeningEnd" && p.next.pointType == "OpeningStart") ||
                        (p.pointType == "OpeningEnd" && p.next.pointType == "WallEnd")) {
                    var spWall = new Wall((Integer.parseInt(wall.id) * mainRatio + wallNum) + "", p.coordinate,
                            p.next.coordinate, wall.type);
                    if (spWall.length < 100)
                        continue;
                    spWalls.add(spWall);
                    wallNum++;
                }
            }
        }

        this.separatedWalls = spWalls;
    }

    /**
     * 获取boundingBox
     */
    private void InitBoundingBox() {
        var points = new ArrayList<Double[]>();
        for (var w : walls) {
            points.add(w.start);
            points.add(w.end);
        }

        this.boundingBox = new BoundingBox(points);
    }

    public static List<Double[]> CadContour2XmlContour(Matrix m, List<Double[]> CadContour) {

        return CadContour.stream().map(m::Transform).collect(Collectors.toList());
    }

    public void GenerateSubWallXML(CAD cad, SemanticMatch match, Double angle, Boolean isLength) {

        var mInvert = Matrix.Inverse(match.matrix);
        var xmlContour = CadContour2XmlContour(mInvert, cad.room.contours);
        var root = doc.getRootElement();
        var cadElement = root.addElement("CAD").addAttribute("Num", xmlContour.size() + "");
        xmlContour.forEach(p -> cadElement.addElement("Point").addAttribute("X", p[0] + "").addAttribute("Y", p[1] + ""));

        var angleElement = root.addElement("ANGLE")
                .addAttribute("XML2CAD", angle + "")
                .addAttribute("North2Y", "0");
        var lengthElement = root.addElement("Length")
                .addAttribute("XmlLength", isLength + "");
        var wallLines = doc.selectNodes("/root/WallInfo/LineData");
//        for (var i = root.elementIterator("/WallInfo/LineData"); i.hasNext(); ) 
        for(var node : wallLines)
        {
            Element element = ((Element) node);
            // do something
            var lineAttr = GetAttrPairs(element);
            // 特殊情况1： 2楼的墙
            if (lineAttr.getOrDefault("storey", "1").equals("2")) {

                AddSubLineElement(element, GetLineByAttrPairs(lineAttr), "2", 0, "UN");
                continue;
            }
            var subLines = this.separatedWalls.stream().filter(w -> Integer.parseInt(w.id) / 256 == Integer.parseInt(lineAttr.get("WallId"))).collect(Collectors.toList());
            Integer subNum = 0;
            for (Wall subLine : subLines) {
                var mappingWalls = CAD.GetMappingWallsByXmlWallId(cad.walls, match.mapping, subLine.id);

                // 特殊情况2： 没有对应的墙
                if (mappingWalls.size() == 0) {
                    AddSubLineElement(element, subLine, "1", subNum, "UN");
                    subNum++;
                    continue;
                }

                // 过滤与主墙 不同方向的墙
                var longWall = mappingWalls.stream().max(Comparator.comparing(Wall::GetLength)).get();
                var filteredWalls = mappingWalls.stream().filter(w -> Geometry.CheckCollinear(longWall, w, 10)).collect(Collectors.toList());

                //特殊情况3： 能匹配到的墙 为同种材质
                var materials = filteredWalls.stream().map(w -> w.type).collect(Collectors.toCollection(HashSet::new));
                if (materials.size() == 1) {
                    var material = filteredWalls.get(0).type.equals("mainWall") ? "CO" : "AL";
                    AddSubLineElement(element, subLine, "1", subNum, material);
                    subNum++;
                }

                // 一般情况 合理简化 扫描的一面含多种类型的墙 为单混凝土墙加其他部分的隔墙
                var mainWall = filteredWalls.stream().filter(w -> w.type.equals("mainWall")).collect(Collectors.toList()).get(0);
                var tStart = mInvert.Transform(mainWall.start);
                var tEnd = mInvert.Transform(mainWall.end);
                var tStartNear = Geometry.GetNearestPointOnTheLine(tStart, subLine);
                var tEndNear = Geometry.GetNearestPointOnTheLine(tEnd, subLine);

                var points = new ArrayList<MixPoint>(Arrays.asList(new MixPoint("", subLine.start, null), new MixPoint("", subLine.end, null), new MixPoint("", tStartNear, null), new MixPoint("", tEndNear, null)));
                Collections.sort(points);
                for (int j = 0; j < points.size() -1; j++) {
                    if(Geometry.IsSamePoint(points.get(j).coordinate, points.get(j+1).coordinate, 0))
                        continue;
                    var subLineSegment = new Line(points.get(j).coordinate, points.get(j+1).coordinate, false);
                    var mtl = j==1? "CO" : "AL";
                    AddSubLineElement(element, subLineSegment, "1", subNum, mtl);
                    subNum++;

                }


            }

        }

        Write();

    }

    private void AddSubLineElement(Element parent, Line line, String storey, int index, String material) {
        parent.addElement("SubLine")
                .addAttribute("StartX", line.start[0] + "")
                .addAttribute("StartY", line.start[1] + "")
                .addAttribute("StartZ", 0 + "")
                .addAttribute("EndX", line.end[0] + "")
                .addAttribute("EndY", line.end[1] + "")
                .addAttribute("EndZ", 0 + "")
                .addAttribute("SublineId", index + "")
                .addAttribute("storey", storey)
                .addAttribute("WallType", material);

    }

    private void Write() {
        try {

            File file = new File("SubWall.xml");

            // 设置格式
            OutputFormat xmlFormat = new OutputFormat();
            xmlFormat.setEncoding("UTF-8");
            xmlFormat.setNewlines(true);
            xmlFormat.setIndent("    ");

            var xmlWriter = new XMLWriter(new FileOutputStream(file), xmlFormat);
            xmlWriter.write(doc);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}
